﻿using System;
using System.Collections;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AutocadExporter.MyCommands))]
//[assembly: ExtensionApplication(typeof(AcadToAnsysDotNet.MyCommands))]
namespace AutocadExporter
{
	public class MyCommands : IExtensionApplication
	{
		#region private fields
		private Point3d _defaultReferencePoint = new Point3d(0, 0, 0);
		private double _defaultScaleRatio = 100.0;
		private double _defaultTolerance = 1.0e-2;
		#endregion

		#region Demo commands
		// The CommandMethod attribute can be applied to any public  member 
		// function of any public class.
		// The function should take no arguments and return nothing.
		// If the method is an intance member then the enclosing class is 
		// intantiated for each document. If the member is a static member then
		// the enclosing class is NOT intantiated.
		//
		// NOTE: CommandMethod has overloads where you can provide helpid and
		// context menu.

		// Modal Command with localized name
		[CommandMethod("MyGroup", "MyCommand", "MyCommandLocal", CommandFlags.Modal)]
		public void MyCommand() // This method can have any name
		{
			Document doc = Application.DocumentManager.MdiActiveDocument;
			if (doc != null)
			{
				var ed = doc.Editor;
				var db = ed.Document.Database;
				Debug.Assert(ed != null && db != null);

				var circle = new Circle(new Point3d(100, 100, 0), Vector3d.ZAxis, 200);
				AppendEntityToDatabase(db, circle);

				var line = new Line(new Point3d(100, 100, 0), new Point3d(300, 300, 0));
				var lineId = AppendEntityToDatabase(db, line);

				using (var trans = db.TransactionManager.StartTransaction())
				{
					var newline = trans.GetObject(lineId, OpenMode.ForWrite);
					newline.Erase();
					trans.Commit();
				}
			}
		}

		[CommandMethod("MyGroup", "MyPickFirst", "MyPickFirstLocal", CommandFlags.Modal | CommandFlags.UsePickSet)]
		public void MyPickFirst() // This method can have any name
		{
			PromptSelectionResult result = Application.DocumentManager.MdiActiveDocument.Editor.GetSelection();
			if (result.Status == PromptStatus.OK)
			{

			}
			else
			{
				// There are no selected entities
				// Put your command code here
			}
		}

		// Application Session Command with localized name
		[CommandMethod("MyGroup", "MySessionCmd", "MySessionCmdLocal", CommandFlags.Modal | CommandFlags.Session)]
		public void MySessionCmd()
		{
			// Put your command code here
		}

		// LispFunction is similar to CommandMethod but it creates a lisp 
		// callable function. Many return types are supported not just string
		// or integer.
		[LispFunction("MyLispFunction", "MyLispFunctionLocal")]
		public int MyLispFunction(ResultBuffer args)
		{
			// Put your command code here

			// Return a value to the AutoCAD Lisp Interpreter
			return 1;
		}
		#endregion

		#region my commands
		/// <summary>
		/// Export curve to ANSYS
		/// </summary>
		[CommandMethod("MyGroup", "ATA", "ATA", CommandFlags.Modal | CommandFlags.UsePickSet)]
		public void ExportToAnsys()
		{
			var doc = AcadApp.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);

			//
			// Get selected objectIds
			//
			var selectionResult = ed.GetSelection();
			if (selectionResult.Status != PromptStatus.OK)
				return;
			var allSelectedIds = selectionResult.Value.GetObjectIds();
			if (allSelectedIds.Length <= 0)
				return;

			//
			// Set the global tolerance
			//
			var defaultGlobalTolerance = Tolerance.Global;
			Tolerance.Global = new Tolerance(_defaultTolerance, _defaultTolerance);

			//
			var exportEntities = GetExportEntities(db, allSelectedIds);
			var exportNodes = exportEntities.Item1;
			var exportLines = exportEntities.Item2;
			var exportArcs = exportEntities.Item3;

			// 
			// Generate ANSYS command stream
			//
			var splitString = "! ************************************************************************";
			var curTime = DateTime.Now;
			var ansysCmdLines = new List<string>()
			{
				splitString,
				"! **** Generated by AutocadToAnsys v2014.11.28 (wudong <wdhust@gmail.com>)",
				$"! ****        at <{curTime.ToShortDateString()} {curTime.ToLongTimeString()}>",
				"/prep7",
				"/uis,msgpop,3",
				"lsel,none,all",
				"*get,kst_,kp,0,num,max   ! Get max keypoint number",
				"kst_=kst_+1",
			};

			// Generate key points
			Debug.Assert(exportNodes.All(node => node.NodeId >= 0));
			Debug.Assert(exportNodes.Distinct(new ExportNodeEqualityComparer()).Count() == exportNodes.Count);
			var xCoords = exportNodes.Select(node => (node.Location.X - _defaultReferencePoint.X) / _defaultScaleRatio);
			var yCoords = exportNodes.Select(node => (node.Location.Y - _defaultReferencePoint.Y) / _defaultScaleRatio);
			var xmin = xCoords.Min();
			var xmax = xCoords.Max();
			var ymin = yCoords.Min();
			var ymax = yCoords.Max();
			ansysCmdLines.AddRange(exportNodes.Select(node =>
				$"k,kst_+{node.NodeId},{(node.Location.X - _defaultReferencePoint.X) / _defaultScaleRatio:F5},{(node.Location.Y - _defaultReferencePoint.Y) / _defaultScaleRatio:F5}"
				));

			// Generate lines
			ansysCmdLines.AddRange(exportLines.Select(line => $"lstr,kst_+{line.StartNode.NodeId},kst_+{line.EndNode.NodeId}"));

			// Generate arcs
			ansysCmdLines.AddRange(exportArcs.Select(arc =>
				$"larc,kst_+{arc.StartNode.NodeId},kst_+{arc.EndNode.NodeId},kst_+{arc.CenterNode.NodeId},{arc.Radius / _defaultScaleRatio:F5}"));

			//
			ansysCmdLines.AddRange(new []
			{
					"kst_=",
					"/uis,msgpop,2",
					$"x_min_ = {xmin:F5}",
					$"x_max_ = {xmax:F5}",
					$"y_min_ = {ymin:F5}",
					$"y_max_ = {ymax:F5}",
					splitString,
					"",
			});

			var cmdString = string.Join(Environment.NewLine, ansysCmdLines.ToArray());
			System.Windows.Forms.Clipboard.SetDataObject(cmdString);
			ed.WriteMessage("\n导出的ANSYS命令流已复制到剪贴板中!");

			//
			// Restore to default tolerance setting
			//
			Tolerance.Global = defaultGlobalTolerance;
		}

		/// <summary>
		/// Export curve to ANSYS
		/// </summary>
		[CommandMethod("MyGroup", "ATF", "ATF", CommandFlags.Modal | CommandFlags.UsePickSet)]
		public void ExportToFile()
		{
			var doc = AcadApp.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);

			//
			// Get selected objectIds
			//
			var selectionResult = ed.GetSelection();
			if (selectionResult.Status != PromptStatus.OK)
				return;
			var allSelectedIds = selectionResult.Value.GetObjectIds();
			if (allSelectedIds.Length <= 0)
				return;

			//
			// Set the global tolerance
			//
			var defGlobalTol = Tolerance.Global;
			Tolerance.Global = new Tolerance(_defaultTolerance, _defaultTolerance);

			//
			var exportEntities = GetExportEntities(db, allSelectedIds);
			var exportNodes = exportEntities.Item1;
			var exportLines = exportEntities.Item2;
			var exportArcs = exportEntities.Item3;

			// 
			// Generate ANSYS command stream
			//
			var ansysCmdLines = new List<string>();

			// Generate key points
			Debug.Assert(exportNodes.All(node => node.NodeId >= 0));
			Debug.Assert(exportNodes.Distinct(new ExportNodeEqualityComparer()).Count() == exportNodes.Count);
			ansysCmdLines.AddRange(exportNodes.Select(node =>
			{
				var pt = node.ScaleTo(_defaultReferencePoint, 1.0/_defaultScaleRatio);
				return $"point,{node.NodeId},{pt.X:F5},{pt.Y:F5}";

			}));

			// Generate lines
			ansysCmdLines.AddRange(exportLines.Select(line => $"line,{line.StartNode.NodeId},{line.EndNode.NodeId}"));

			// Generate arcs
			ansysCmdLines.AddRange(exportArcs.Select(arc => $"arc,{arc.CenterNode.NodeId},{arc.StartNode.NodeId},{(arc.Angle*180/Math.PI):F5}"));

			ansysCmdLines.Add("");

			var cmdString = string.Join(Environment.NewLine, ansysCmdLines.ToArray());
			System.Windows.Forms.Clipboard.SetDataObject(cmdString);
			ed.WriteMessage("\n导出的图形信息已复制到剪贴板中!");

			//
			// Restore to default tolerance setting
			//
			Tolerance.Global = defGlobalTol;
		}

		/// <summary>
		/// Query the range of a specified selection region
		/// </summary>
		[CommandMethod("MyGroup", "ATR", "ATR", CommandFlags.Modal)]
		public void QuerySelRange()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);
			var pointResult = ed.GetPoint(new PromptPointOptions("\n选择查询范围的第一个点: ")
			{
				UseBasePoint = true,
				BasePoint = this._defaultReferencePoint
			});
			if (pointResult.Status != PromptStatus.OK)
				return;
			var queryPt1 = pointResult.Value;

			pointResult = ed.GetPoint(new PromptPointOptions("\n选择查询范围的第二个点: ")
			{
				UseBasePoint = true,
				BasePoint = this._defaultReferencePoint
			});
			if (pointResult.Status != PromptStatus.OK)
				return;
			var queryPt2 = pointResult.Value;

			var refPt = this._defaultReferencePoint;
			var scaleRatio = this._defaultScaleRatio;
			var queryResult = $"\nsel,s,loc,x,{(queryPt1.X - refPt.X)/scaleRatio:F1},{(queryPt2.X - refPt.X)/scaleRatio:F1}"
			                  + $"\nsel,r,loc,y,{(queryPt1.Y - refPt.Y)/scaleRatio:F1},{(queryPt2.Y - refPt.Y)/scaleRatio:F1}";
			System.Windows.Forms.Clipboard.SetDataObject(queryResult);
			ed.WriteMessage("\n范围查询结果(已复制到剪贴板): " + queryResult);
		}

		/// <summary>
		/// Query the coordinate of a specified point
		/// </summary>
		[CommandMethod("MyGroup", "ATC", "ATC", CommandFlags.Modal)]
		public void QueryPointCoordinate()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);
			var pointResult = ed.GetPoint(new PromptPointOptions("\n选择需查询坐标的点: ")
			{
				UseBasePoint = true,
				BasePoint = this._defaultReferencePoint
			});
			if (pointResult.Status != PromptStatus.OK)
				return;
			var queryPt = pointResult.Value;
			var refPt = this._defaultReferencePoint;
			var scaleRatio = this._defaultScaleRatio;
			var coordString = $"{(queryPt.X - refPt.X)/scaleRatio:F2},{(queryPt.Y - refPt.Y)/scaleRatio:F2}";
			ed.WriteMessage("\n" + coordString);

			System.Windows.Forms.Clipboard.SetDataObject(coordString);
		}

		/// <summary>
		/// Query the coordinate of a specified point
		/// </summary>
		[CommandMethod("MyGroup", "ATMC", "ATMC", CommandFlags.Modal)]
		public void QueryMultiplePointCoordinate()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);
			var selectedPoints = new List<Point3d>();
			while (true)
			{
				var pointResult = ed.GetPoint(new PromptPointOptions("\n选择需查询坐标的点: ")
				{
					UseBasePoint = true,
					BasePoint = this._defaultReferencePoint,
				});
				if (pointResult.Status == PromptStatus.OK)
				{
					selectedPoints.Add(pointResult.Value);
				}
				else
				{
					break;
				}
			}

			if (selectedPoints.Count <= 0)
			{
				return;
			}

			var refPt = this._defaultReferencePoint;
			var scaleRatio = this._defaultScaleRatio;
			var coordList = selectedPoints.Select(pt =>
				$"{(pt.X - refPt.X)/scaleRatio:F2}, {(pt.Y - refPt.Y)/scaleRatio:F2}");
			
			ed.WriteMessage("\n");
			ed.WriteMessage(string.Join("\n", coordList));

			var coordString = string.Join(Environment.NewLine, coordList) + Environment.NewLine;
			System.Windows.Forms.Clipboard.SetDataObject(coordString);
		}

		/// <summary>
		/// Query the coordinate of a specified point
		/// </summary>
		[CommandMethod("MyGroup", "ATN", "ATN", CommandFlags.Modal)]
		public void QueryLineInfo()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);
			
			var selectionResult = ed.GetSelection(new PromptSelectionOptions()
			{
				AllowDuplicates = false,
			});
			if (selectionResult.Status != PromptStatus.OK)
				return;
			var allSelectedIds = selectionResult.Value.GetObjectIds().Distinct().ToList();
			if (allSelectedIds.Count <= 0)
				return;

			var selectedLines = new List<Line>();
			using (var trans = db.TransactionManager.StartTransaction())
			{
				foreach (var id in allSelectedIds)
				{
					var curve = trans.GetObject(id, OpenMode.ForRead) as Curve;
					Debug.Assert(curve != null);
					if (curve is Line)
					{
						var line = (Line)trans.GetObject(id, OpenMode.ForRead);
						if(line.Length > this._defaultTolerance)
							selectedLines.Add(line);
					}
				}
			}
			if (selectedLines.Count <= 0)
				return;

			var refPt = this._defaultReferencePoint;
			var scaleRatio = this._defaultScaleRatio;
			var outputString = string.Join(Environment.NewLine, selectedLines.Select(line =>
			{
				var startPoint = line.StartPoint;
				var endPoint = line.EndPoint;
				return "sel beam id 1 "
				       + $"begin ({(startPoint.X - refPt.X)/scaleRatio:F2},{(startPoint.Y - refPt.Y)/scaleRatio:F2},@z0) "
				       + $"end ({(endPoint.X - refPt.X)/scaleRatio:F2},{(endPoint.Y - refPt.Y)/scaleRatio:F2},@z0) nseg 1";
			}));
			ed.WriteMessage("\n" + outputString);

			System.Windows.Forms.Clipboard.SetDataObject(outputString);
		}

		/// <summary>
		/// Set the global options of AutocadToAnsys
		/// </summary>
		[CommandMethod("MyGroup", "ATS", "ATS", CommandFlags.Modal)]
		public void SetOptions()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			Debug.Assert(doc != null);
			var ed = doc.Editor;
			var db = doc.Database;
			Debug.Assert(ed != null && db != null);

			//
			// set reference point
			//
			var pointResult = ed.GetPoint(new PromptPointOptions(
				$"\n选择原点: <{PointHelper.GetPoint3dFormatedString(this._defaultReferencePoint)}>")
			{
				AllowNone = true
			});
			if ((pointResult.Status == PromptStatus.OK || pointResult.Status == PromptStatus.None) == false)
				return;
			if (pointResult.Status == PromptStatus.OK)
				this._defaultReferencePoint = pointResult.Value;

			//
			// set scale ratio
			//
			var scaleRatioResult = ed.GetDouble(new PromptDoubleOptions(string.Format("\n输入缩放比例系数: "))
			{
				DefaultValue = this._defaultScaleRatio,
				AllowNegative = false,
				AllowZero = false,
				AllowNone = true
			});
			if ((scaleRatioResult.Status == PromptStatus.OK || scaleRatioResult.Status == PromptStatus.None) == false)
				return;
			if (scaleRatioResult.Status == PromptStatus.OK)
				_defaultScaleRatio = scaleRatioResult.Value;

			//
			// set tolerance
			//
			var toleranceResult = ed.GetDouble(new PromptDoubleOptions(string.Format("\n输入容差值: "))
			{
				DefaultValue = this._defaultTolerance,
				AllowNegative = false,
				AllowZero = false,
				AllowNone = true
			});
			if ((toleranceResult.Status == PromptStatus.OK || toleranceResult.Status == PromptStatus.None) == false)
				return;
			if (toleranceResult.Status == PromptStatus.OK)
				_defaultTolerance = toleranceResult.Value;
		}
		#endregion

		#region helper methods
		private ObjectId AppendEntityToDatabase(Database db, Entity entity)
		{
			ObjectId id = ObjectId.Null;
			using (var trans = db.TransactionManager.StartTransaction())
			{
				var bt = trans.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord;
				Debug.Assert(bt != null);
				id = bt.AppendEntity(entity);
				trans.AddNewlyCreatedDBObject(entity, true);
				trans.Commit();
			}
			return id;
		}

		private static double NormalizeAngle(double angle)
		{
			const double PI2 = Math.PI * 2;
			return angle - Math.Floor(angle / PI2) * PI2;
		}

		private static Tuple<List<ExportNode>, List<ExportLine>, List<ExportArc>> GetExportEntities(Database db, IEnumerable<ObjectId> allSelectedIds)
		{
			//
			// Get selected curves
			//
			var selectedLineIds = new List<ObjectId>();
			var selectedArcIds = new List<ObjectId>();
			var selectedCircleIds = new List<ObjectId>();
			using (var trans = db.TransactionManager.StartTransaction())
			{
				foreach (var id in allSelectedIds)
				{
					var curve = trans.GetObject(id, OpenMode.ForRead) as Curve;
					Debug.Assert(curve != null);
					if (curve is Line)
					{
						selectedLineIds.Add(id);
					}
					else if (curve is Arc)
					{
						selectedArcIds.Add(id);
					}
					else if (curve is Circle)
					{
						selectedCircleIds.Add(id);
					}
				}
			}

			var selectedCurveIds = selectedLineIds.Concat(selectedArcIds).Concat(selectedCircleIds).ToList();
			var exportNodes = new List<ExportNode>();
			var exportLines = new List<ExportLine>();
			var exportArcs = new List<ExportArc>();
			using (var trans = db.TransactionManager.StartTransaction())
			{
				foreach (var selectedCircleId in selectedCircleIds)     // for each selected circle
				{
					var thisCircle = trans.GetObject(selectedCircleId, OpenMode.ForRead) as Circle;
					Debug.Assert(thisCircle != null);
					var intersectPoints = new List<Point3d>();
					foreach (var selectedCurveId in selectedCurveIds)
					{
						if (selectedCurveId == selectedCircleId)
							continue;

						var otherCurve = trans.GetObject(selectedCurveId, OpenMode.ForRead) as Curve;
						Debug.Assert(otherCurve != null);
						var interPts = new Point3dCollection();
						var projectPlane = new Plane(new Point3d(0, 0, 0), new Vector3d(0, 0, 1));
						thisCircle.IntersectWith(otherCurve, Intersect.OnBothOperands,
							projectPlane, interPts, IntPtr.Zero, IntPtr.Zero);
						intersectPoints.AddRange(interPts.Cast<Point3d>());
					}

					var interPtAngles = intersectPoints.Select(pt => NormalizeAngle(thisCircle.GetParameterAtPoint(pt)))
						.Distinct().OrderBy(angle => angle).ToList();
					if (interPtAngles.Count <= 0)
					{
						interPtAngles.AddRange(Enumerable.Range(0, 4).Select(i => NormalizeAngle(i * 0.5 * Math.PI)));
					}
					else if (interPtAngles.Count == 1)
					{
						var startAngle = interPtAngles[0];
						interPtAngles.AddRange(Enumerable.Range(0, 3).Select(i => NormalizeAngle(startAngle + (i + 1) * 0.5 * Math.PI)));
					}
					else
					{
						var angles = interPtAngles.Concat(new[] { interPtAngles[0] + 2.0 * Math.PI }).ToList();
						var divAngles = new List<double>();
						for (int i = 0; i < angles.Count - 1; i++)
						{
							var startAngle = angles[i];
							var endAngle = angles[i + 1];
							Debug.Assert(0 <= startAngle && startAngle < 2.0 * Math.PI && startAngle < endAngle);
							var divPtCount = (int)Math.Floor((endAngle - startAngle) / Math.PI);
							if (divPtCount >= 1)
							{
								var da = (endAngle - startAngle) / (divPtCount + 1);
								divAngles.AddRange(Enumerable.Range(0, divPtCount).Select(j => NormalizeAngle(startAngle + (j + 1) * da)));
							}
						}
						interPtAngles.AddRange(divAngles);
					}
					interPtAngles = interPtAngles.Distinct().OrderBy(angle => angle).ToList();
					interPtAngles = interPtAngles.Concat(interPtAngles.GetRange(0, 1)).ToList();
					var arcPoints = interPtAngles.Select(angle => thisCircle.GetPointAtParameter(angle)).ToList();
					for (int i = 0; i < arcPoints.Count - 1; i++)
					{
						var startPoint = arcPoints[i];
						var endPoint = arcPoints[i + 1];
						var startNode = exportNodes.FindOrAdd(startPoint);
						var endNode = exportNodes.FindOrAdd(endPoint);
						var centerNode = exportNodes.FindOrAdd(thisCircle.Center);
						if (!startNode.IsSameNodeWith(endNode))
						{
							exportArcs.Add(new ExportArc()
							{
								CenterNode = centerNode,
								StartNode = startNode,
								EndNode = endNode,
								Radius = thisCircle.Radius
							});
						}
					}
				}

				foreach (var selectedArcId in selectedArcIds)
				{
					var thisArc = trans.GetObject(selectedArcId, OpenMode.ForRead) as Arc;
					Debug.Assert(thisArc != null);
					var intersectPoints = new List<Point3d>();
					foreach (var selectedCurveId in selectedCurveIds)
					{
						if (selectedCurveId == selectedArcId)
							continue;

						var otherCurve = trans.GetObject(selectedCurveId, OpenMode.ForRead) as Curve;
						Debug.Assert(otherCurve != null);
						var interPts = new Point3dCollection();
						var projectPlane = new Plane(new Point3d(0, 0, 0), new Vector3d(0, 0, 1));
						thisArc.IntersectWith(otherCurve, Intersect.OnBothOperands,
							projectPlane, interPts, IntPtr.Zero, IntPtr.Zero);
						intersectPoints.AddRange(interPts.Cast<Point3d>()
							.Where(pt => !PointHelper.IsSameLocation(pt, thisArc.StartPoint)
								&& !PointHelper.IsSameLocation(pt, thisArc.EndPoint)));
					}

					var interPtAngles = intersectPoints.Select(pt => thisArc.GetParameterAtPoint(pt))
						.Concat(new[] { thisArc.StartParam, thisArc.EndParam })
						.Distinct()
						.OrderBy(angle => angle).ToList();
					var divAngles = new List<double>();
					for (int i = 0; i < interPtAngles.Count - 1; i++)
					{
						var startAngle = interPtAngles[i];
						var endAngle = interPtAngles[i + 1];
						var divPtCount = (int)Math.Floor((endAngle - startAngle) / (0.5 * Math.PI));
						if (divPtCount >= 1)
						{
							var da = (endAngle - startAngle) / (divPtCount + 1);
							divAngles.AddRange(Enumerable.Range(0, divPtCount).Select(j => startAngle + (j + 1) * da));
						}
					}
					interPtAngles.AddRange(divAngles);
					interPtAngles = interPtAngles.Distinct().OrderBy(angle => angle).ToList();
					for (int i = 0; i < interPtAngles.Count - 1; i++)
					{
						var startAngle = interPtAngles[i];
						var endAngle = interPtAngles[i + 1];
						var startPoint = thisArc.GetPointAtParameter(startAngle);
						var endPoint = thisArc.GetPointAtParameter(endAngle);
						var startNode = exportNodes.FindOrAdd(startPoint);
						var endNode = exportNodes.FindOrAdd(endPoint);
						var centerNode = exportNodes.FindOrAdd(thisArc.Center);
						if (!startNode.IsSameNodeWith(endNode))
						{
							exportArcs.Add(new ExportArc()
							{
								CenterNode = centerNode,
								StartNode = startNode,
								EndNode = endNode,
								Radius = thisArc.Radius
							});
						}
					}
				}

				foreach (var selectedLineId in selectedLineIds)
				{
					var thisLine = trans.GetObject(selectedLineId, OpenMode.ForRead) as Line;
					Debug.Assert(thisLine != null);
					var intersectPoints = new List<Point3d>();
					foreach (var selectedCurveId in selectedCurveIds)
					{
						if (selectedCurveId == selectedLineId)
							continue;

						var otherCurve = trans.GetObject(selectedCurveId, OpenMode.ForRead) as Curve;
						Debug.Assert(otherCurve != null);
						var interPts = new Point3dCollection();
						var projectPlane = new Plane(new Point3d(0, 0, 0), Vector3d.ZAxis);
						thisLine.IntersectWith(otherCurve, Intersect.OnBothOperands,
							projectPlane, interPts, IntPtr.Zero, IntPtr.Zero);
						intersectPoints.AddRange(interPts.Cast<Point3d>()
							.Where(pt => !PointHelper.IsSameLocation(pt, thisLine.StartPoint)
								&& !PointHelper.IsSameLocation(pt, thisLine.EndPoint)));
					}

					var interPtParams = intersectPoints.Select(pt => thisLine.GetParameterAtPoint(pt))
						.Concat(new[] { thisLine.StartParam, thisLine.EndParam })
						.Distinct()
						.OrderBy(param => param).ToList();
					for (int i = 0; i < interPtParams.Count - 1; i++)
					{
						var startPoint = thisLine.GetPointAtParameter(interPtParams[i]);
						var endPoint = thisLine.GetPointAtParameter(interPtParams[i + 1]);
						var startNode = exportNodes.FindOrAdd(startPoint);
						var endNode = exportNodes.FindOrAdd(endPoint);
						if (!startNode.IsSameNodeWith(endNode))
						{
							exportLines.Add(new ExportLine() { StartNode = startNode, EndNode = endNode });
						}
					}
				}
			}

			return Tuple.Create(exportNodes, exportLines, exportArcs);
		}
		#endregion

		void IExtensionApplication.Initialize()
		{
			var doc = Application.DocumentManager.MdiActiveDocument;
			var ed = doc.Editor;
			ed.WriteMessage("AutocadExporter is initialized successfully! \n");
		}

		void IExtensionApplication.Terminate() { }
	}

	public static class PointHelper
	{
		public static string GetPoint3dFormatedString(Point3d pt)
		{
			return $"({pt.X:F3},{pt.Y:F3},{pt.Z:F3})";
		}

		public static bool IsPointOnCurve(Curve cv, Point3d pt)
		{
			try
			{
				var p = cv.GetClosestPointTo(pt, false);
				return (p - pt).Length <= Tolerance.Global.EqualPoint;
			}
			catch { }
			return false;
		}

		public static double PointInPlaneDistance(Point3d pt1, Point3d pt2)
		{
			var dx = pt1.X - pt2.X;
			var dy = pt1.Y - pt2.Y;
			return Math.Sqrt(dx * dx + dy * dy);
		}

		public static bool IsSameLocation(Point3d pt1, Point3d pt2)
		{
			return PointInPlaneDistance(pt1, pt2) < Tolerance.Global.EqualPoint;
		}
	}

	public class ExportNode
	{
		public int NodeId { get; set; } = -1;
		public Point3d Location { get; set; }

		public override string ToString()
		{
			return $"{this.NodeId:D}_{this.Location.ToString()}";
		}
		public bool IsSameNodeWith(ExportNode otherNode)
		{
			Debug.Assert(this.NodeId >= 0 && otherNode.NodeId >= 0);
			if (this.NodeId == otherNode.NodeId)
			{
				Debug.Assert(PointHelper.PointInPlaneDistance(this.Location, otherNode.Location) < Tolerance.Global.EqualPoint);
				return true;
			}
			else
			{
				return false;
			}
		}
		public bool IsSameLocationWith(ExportNode otherNode)
		{
			return PointHelper.PointInPlaneDistance(this.Location, otherNode.Location) < Tolerance.Global.EqualPoint;
		}
		public bool IsSameLocationWith(Point3d pt)
		{
			return PointHelper.PointInPlaneDistance(this.Location, pt) < Tolerance.Global.EqualPoint;
		}

		public Point3d ScaleTo(Point3d refPoint, double scaleRatio)
			=> new Point3d((Location.X - refPoint.X)*scaleRatio, (Location.Y - refPoint.Y)*scaleRatio,
					(Location.Z - refPoint.Z)*scaleRatio);

		public double AngleWith(ExportNode other)
		{
			Debug.Assert(this.Location.DistanceTo(other.Location) > 1.0e-6);
			var angle = Math.Atan2(other.Location.Y - this.Location.Y, other.Location.X - this.Location.X);
			const double pi2 = Math.PI*2;
			if (angle < 0)
			{
				angle += pi2;
			}
			if (angle >= pi2)
			{
				angle -= pi2;
			}
			Debug.Assert(0 <= angle && angle < pi2);
			return angle;
		}
	}

	public class ExportLine
	{
		public ExportNode StartNode { get; set; }
		public ExportNode EndNode { get; set; }
	}

	public class ExportArc
	{
		public ExportNode StartNode { get; set; }
		public ExportNode EndNode { get; set; }
		public ExportNode CenterNode { get; set; }
		public double Radius { get; set; }

		public double Angle
		{
			get
			{
				var angle = CenterNode.AngleWith(EndNode) - CenterNode.AngleWith(StartNode);
				const double pi2 = Math.PI*2;
				if (angle < 0)
				{
					angle += pi2;
				}
				if (angle >= pi2)
				{
					angle -= pi2;
				}
				Debug.Assert(0 <= angle && angle < pi2);
				return angle;
			}
		}
	}

	public class ExportNodeEqualityComparer : IEqualityComparer<ExportNode>
	{
		bool IEqualityComparer<ExportNode>.Equals(ExportNode node1, ExportNode node2)
		{
			return node1.IsSameNodeWith(node2);
		}

		int IEqualityComparer<ExportNode>.GetHashCode(ExportNode node)
		{
			return $"{node.Location.X:F3}_{node.Location.Y:F3}_{node.Location.Z:F3}_{node.NodeId:D}"
				.GetHashCode();
		}
	}

	public static class ExportNodeListExtension
	{
		public static ExportNode FindOrAdd(this List<ExportNode> exportNodes, Point3d pt)
		{
			Debug.Assert(exportNodes != null);
			var nodeCount = exportNodes.Count;
			if (nodeCount <= 0)
			{
				var node = new ExportNode() { NodeId = 0, Location = pt };
				exportNodes.Add(node);
				return node;
			}
			else
			{
				var exsitingNode = exportNodes.Find(en => en.IsSameLocationWith(pt));
				if (exsitingNode != null)
				{
					return exsitingNode;
				}
				else
				{
					var node = new ExportNode() { NodeId = exportNodes[nodeCount - 1].NodeId + 1, Location = pt };
					exportNodes.Add(node);
					return node;
				}
			}

		}
	}
}
